/*
 * Copyright (C) 2016-18 Kaspar Schleiser <kaspar@schleiser.de>
 *               2018 Inria
 *               2018 Freie Universität Berlin
 *
 * This file is subject to the terms and conditions of the GNU Lesser
 * General Public License v2.1. See the file LICENSE in the top level
 * directory for more details.
 */

/**
 * @ingroup     net_nanocoap
 * @{
 *
 * @file
 * @brief       nanoCoAP sock helpers
 *
 * @author      Kaspar Schleiser <kaspar@schleiser.de>
 * @author      Benjamin Valentin <benjamin.valentin@ml-pa.com>
 *
 * @}
 */

#include <errno.h>
#include <string.h>
#include <stdio.h>

#include "container.h"
#include "event/thread.h"
#include "net/credman.h"
#include "net/nanocoap.h"
#include "net/nanocoap_sock.h"
#ifdef MODULE_SOCK_ASYNC_EVENT
#include "net/sock/async/event.h"
#endif
#include "net/sock/udp.h"
#include "net/sock/util.h"
#include "random.h"
#include "sys/uio.h" /* IWYU pragma: keep (exports struct iovec) */
#include "ztimer.h"

#define ENABLE_DEBUG 0
#include "debug.h"

/**
 * @brief   Size of the buffer used for the DTLS handshake
 *
 * This size was found suitable for DTLS using a simple PSK in mode AES_128_CCM_8.
 * DTLS places no restriction on its handshake package size therefore this might need change,
 * if mode or key-size change especially if certificates instead of PSK are used.
 */
#ifndef CONFIG_NANOCOAP_DTLS_HANDSHAKE_BUF_SIZE
#  define CONFIG_NANOCOAP_DTLS_HANDSHAKE_BUF_SIZE (160)
#endif

#ifndef CONFIG_NANOCOAP_MAX_OBSERVERS
#  define CONFIG_NANOCOAP_MAX_OBSERVERS 4
#endif

enum {
    STATE_REQUEST_SEND,     /**< request was just sent or will be sent again */
    STATE_STOP_RETRANSMIT,  /**< stop retransmissions due to a matching empty ACK */
    STATE_WAIT_RESPONSE,    /**< waiting for a response */
};

typedef struct {
    coap_blockwise_cb_t callback;
    void *arg;
    uint32_t blknum;
    bool more;
#if CONFIG_NANOCOAP_SOCK_BLOCK_TOKEN
    uint8_t token[4];
#endif
} _block_ctx_t;

/**
 * @brief   Structure to track the state of an observation
 */
typedef struct {
    /**
     * @brief   Context needed to build notifications (e.g. Token, endpoint
     *          to send to)
     *
     * @details To safe ROM, we reuse the separate response code to also
     *          send notifications, as the functionality is almost identical.
     */
    nanocoap_server_response_ctx_t response;
    /**
     * @brief   The resource the client has subscribed to
     *
     * @details This is `NULL` when the slot is free
     */
    const coap_resource_t *resource;
    /**
     * @brief   Message ID used in the last notification
     */
    uint16_t msg_id;
} _observer_t;

#if MODULE_NANOCOAP_SERVER_OBSERVE
static _observer_t _observer_pool[CONFIG_NANOCOAP_MAX_OBSERVERS];
static mutex_t _observer_pool_lock;
#endif

int nanocoap_sock_dtls_connect(nanocoap_sock_t *sock, sock_udp_ep_t *local,
                               const sock_udp_ep_t *remote, credman_tag_t tag)
{
#if IS_USED(MODULE_NANOCOAP_DTLS)
    uint8_t buf[CONFIG_NANOCOAP_DTLS_HANDSHAKE_BUF_SIZE];

    sock->type = COAP_SOCKET_TYPE_DTLS;
    return sock_dtls_establish_session(&sock->udp, &sock->dtls, &sock->dtls_session,
                                       tag, local, remote, buf, sizeof(buf));
#else
    (void)sock;
    (void)local;
    (void)remote;
    (void)tag;
    return -ENOTSUP;
#endif
}

static int _get_error(const coap_pkt_t *pkt)
{
    switch (coap_get_code_class(pkt)) {
    case COAP_CLASS_CLIENT_FAILURE:
        return -ENXIO;
    case COAP_CLASS_SERVER_FAILURE:
        return -ENETRESET;
    default:
        return 0;
    }
}

static inline nanocoap_socket_type_t _get_type(nanocoap_sock_t *sock)
{
#if IS_USED(MODULE_NANOCOAP_DTLS)
    return sock->type;
#else
    (void)sock;
    return COAP_SOCKET_TYPE_UDP;
#endif
}

static int _sock_sendv(nanocoap_sock_t *sock, const iolist_t *snips)
{
    switch (_get_type(sock)) {
    case COAP_SOCKET_TYPE_UDP:
        return sock_udp_sendv(&sock->udp, snips, NULL);
#if IS_USED(MODULE_NANOCOAP_DTLS)
    case COAP_SOCKET_TYPE_DTLS:
        return sock_dtls_sendv(&sock->dtls, &sock->dtls_session, snips,
                               CONFIG_SOCK_DTLS_TIMEOUT_MS);
#endif
    default:
        assert(0);
        return -EINVAL;
    }
}

static int _sock_recv_buf(nanocoap_sock_t *sock, void **data, void **ctx, uint32_t timeout)
{
    switch (_get_type(sock)) {
    case COAP_SOCKET_TYPE_UDP:
        return sock_udp_recv_buf(&sock->udp, data, ctx, timeout, NULL);
#if IS_USED(MODULE_NANOCOAP_DTLS)
    case COAP_SOCKET_TYPE_DTLS:
        return sock_dtls_recv_buf(&sock->dtls, &sock->dtls_session, data, ctx, timeout);
#endif
    default:
        assert(0);
        return -EINVAL;
   }
}

static int _send_ack(nanocoap_sock_t *sock, coap_pkt_t *pkt)
{
    coap_hdr_t ack;

    const iolist_t snip = {
        .iol_base = &ack,
        .iol_len  = sizeof(ack),
    };

    coap_build_empty_ack(pkt, &ack);

    return _sock_sendv(sock, &snip);
}

static bool _id_or_token_missmatch(const coap_pkt_t *pkt, unsigned id,
                                   const void *token, size_t token_len)
{
    switch (coap_get_type(pkt)) {
    case COAP_TYPE_RST:
    case COAP_TYPE_ACK:
        /* message ID only has to match for RST and ACK */
        if (coap_get_id(pkt) != id) {
            return true;
        }
        /* falls through */
    default:
        /* token has to match if message is not empty */
        if (coap_get_code_raw(pkt) != 0) {
            if (coap_get_token_len(pkt) != token_len) {
                return true;
            }
            return memcmp(coap_get_token(pkt), token, token_len);
        }
        else {
            /* but only RST and ACK may be empty */
            return coap_get_type(pkt) != COAP_TYPE_RST &&
                   coap_get_type(pkt) != COAP_TYPE_ACK;
        }
    }
}

static uint32_t _deadline_from_interval(uint32_t interval)
{
    return US_PER_MS * ztimer_now(ZTIMER_MSEC) + interval;
}

static uint32_t _deadline_left_us(uint32_t deadline)
{
    uint32_t now = ztimer_now(ZTIMER_MSEC) * US_PER_MS;
    if (now > deadline) {
        return 0;
    }

    return deadline - now;
}

static void _sock_flush(nanocoap_sock_t *sock)
{
    void *payload, *ctx = NULL;
    while (_sock_recv_buf(sock, &payload, &ctx, 0) > 0 || ctx) {}
}

ssize_t nanocoap_sock_request_cb(nanocoap_sock_t *sock, coap_pkt_t *pkt,
                                 coap_request_cb_t cb, void *arg)
{
    ssize_t tmp, res = 0;
    const unsigned id = coap_get_id(pkt);
    void *payload, *ctx = NULL;
    const uint8_t *token = coap_get_token(pkt);
    uint8_t token_len = coap_get_token_len(pkt);
    uint8_t state = STATE_REQUEST_SEND;

    /* random timeout, deadline for receive retries */
    uint32_t timeout = random_uint32_range((uint32_t)CONFIG_COAP_ACK_TIMEOUT_MS * US_PER_MS,
                                           (uint32_t)CONFIG_COAP_ACK_TIMEOUT_MS * CONFIG_COAP_RANDOM_FACTOR_1000);
    uint32_t deadline = _deadline_from_interval(timeout);

    /* check if we expect a reply */
    const bool confirmable = coap_get_type(pkt) == COAP_TYPE_CON;

    /* add 1 for initial transmit, retry only when CONfirmable */
    unsigned tries_left = confirmable * CONFIG_COAP_MAX_RETRANSMIT + 1;

    /* Create the first payload snip from the request buffer */
    iolist_t head = {
        .iol_next = pkt->snips,
        .iol_base = pkt->buf,
        .iol_len  = coap_get_total_len(pkt),
    };

    /* clear out stale responses from previous requests */
    _sock_flush(sock);

    while (1) {
        switch (state) {
        case STATE_REQUEST_SEND:
            assert(tries_left > 0);
            --tries_left;

            DEBUG("nanocoap: send %u bytes (%u tries left)\n",
                  (unsigned)iolist_size(&head), tries_left);

            res = _sock_sendv(sock, &head);
            if (res <= 0) {
                DEBUG("nanocoap: error sending coap request, %" PRIdSIZE "\n", res);
                goto release;
            }

            /* no response needed and no response handler given */
            if (!confirmable && !cb) {
                res = 0;
                goto release;
            }

            /* ctx must have been released at this point */
            assert(ctx == NULL);
            state = STATE_WAIT_RESPONSE;
            /* fall-through */
        case STATE_WAIT_RESPONSE:           /* waiting for response, no empty ACK received */
        case STATE_STOP_RETRANSMIT:         /* waiting for response, empty ACK received */
            if (ctx == NULL) {
                DEBUG("nanocoap: waiting for response (timeout: %"PRIu32" µs)\n",
                      _deadline_left_us(deadline));
            }
            const void *old_ctx = ctx;
            tmp = _sock_recv_buf(sock, &payload, &ctx, _deadline_left_us(deadline));
            /* sock_udp_recv_buf() is supposed to return multiple packet fragments
             * when called multiple times with the same context.
             * In practise, this is not implemented and it will always return a pointer
             * to the whole packet on the first call and NULL on the second call, which
             * releases the packet.
             * This assertion will trigger should the behavior change in the future.
             */
            if (old_ctx) {
                assert(tmp == 0 && ctx == NULL);
            }
            if (tmp == 0) {
                /* no more data */
                /* sock_udp_recv_buf() needs to be called in a loop until ctx is NULL again
                 * to release the buffer */
                continue;
            }
            res = tmp;
            if (res == -ETIMEDOUT) {
                if (tries_left == 0) {
                    DEBUG("nanocoap: maximum retries reached\n");
                    goto release;
                }
                state = STATE_REQUEST_SEND;
                DEBUG("nanocoap: timeout waiting for response\n");
                timeout *= 2;
                deadline = _deadline_from_interval(timeout);
                continue;
            }
            if (res < 0) {
                DEBUG("nanocoap: error receiving CoAP response, %" PRIdSIZE "\n", res);
                goto release;
            }

            /* parse response */
            if (coap_parse_udp(pkt, payload, res) < 0) {
                DEBUG("nanocoap: error parsing packet\n");
                continue;
            }
            else if (_id_or_token_missmatch(pkt, id, token, token_len)) {
                DEBUG("nanocoap: ID mismatch, got %u want %u\n", coap_get_id(pkt), id);
                continue;
            }
            DEBUG("nanocoap: response code=%i\n", coap_get_code_decimal(pkt));
            switch (coap_get_type(pkt)) {
            case COAP_TYPE_ACK:
                if (coap_get_code_raw(pkt) == COAP_CODE_EMPTY) {
                    /* empty ACK, wait for separate response */
                    state = STATE_STOP_RETRANSMIT;
                    deadline = _deadline_from_interval(CONFIG_COAP_SEPARATE_RESPONSE_TIMEOUT_MS
                                                     * US_PER_MS);
                    tries_left = 0; /* stop retransmissions */
                    DEBUG("nanocoap: wait for separate response\n");
                    continue;
                }
                /* fall-through */
            case COAP_TYPE_RST:
            case COAP_TYPE_CON:
            case COAP_TYPE_NON:
                if (coap_get_type(pkt) == COAP_TYPE_RST) {
                    /* think about whether cb should be called on RST as well */
                    res = -EBADMSG;
                    goto release;
                }
                if (coap_get_type(pkt) == COAP_TYPE_CON) {
                    _send_ack(sock, pkt);
                }
                if (cb) {
                    res = cb(arg, pkt);
                }
                else {
                    res = _get_error(pkt);
                }
                goto release;
            }
        }
    }

release:
    while (ctx) {
        /* make sure ctx is really deleted in all cases */
        _sock_recv_buf(sock, &payload, &ctx, 0);
    }

    return res;
}

static int _request_cb(void *arg, coap_pkt_t *pkt)
{
    struct iovec *buf = arg;
    size_t pkt_len = coap_get_total_len(pkt);

    int res = _get_error(pkt);
    if (res) {
        return res;
    }

    if (pkt_len > buf->iov_len) {
        return -ENOBUFS;
    }

    memcpy(buf->iov_base, pkt->buf, pkt_len);

    pkt->buf = buf->iov_base;
    pkt->payload = pkt->buf + (pkt_len - pkt->payload_len);

    return pkt_len;
}

ssize_t nanocoap_sock_request(nanocoap_sock_t *sock, coap_pkt_t *pkt, size_t len)
{
    struct iovec buf = {
        .iov_base = pkt->buf,
        .iov_len  = len,
    };
    return nanocoap_sock_request_cb(sock, pkt, _request_cb, &buf);
}

static int _get_put_cb(void *arg, coap_pkt_t *pkt)
{
    struct iovec *buf = arg;

    int res = _get_error(pkt);
    if (res) {
        return res;
    }

    if (pkt->payload_len > buf->iov_len) {
        return -ENOBUFS;
    }

    memcpy(buf->iov_base, pkt->payload, pkt->payload_len);

    return pkt->payload_len;
}

static ssize_t _sock_get(nanocoap_sock_t *sock, const char *path,
                         uint8_t type,
                         void *response, size_t max_len)
{
    uint8_t *pktpos = sock->hdr_buf;

    coap_pkt_t pkt = {
        .buf = pktpos,
    };

    struct iovec ctx = {
        .iov_base = response,
        .iov_len  = max_len,
    };

    ssize_t hdr_len = coap_build_udp_hdr(sock->hdr_buf, sizeof(sock->hdr_buf), type, NULL, 0, COAP_METHOD_GET,
                                         nanocoap_sock_next_msg_id(sock));
    assume(hdr_len > 0);
    pktpos += hdr_len;
    pktpos += coap_opt_put_uri_pathquery(pktpos, NULL, path);
    assert(pktpos < (uint8_t *)sock->hdr_buf + sizeof(sock->hdr_buf));

    pkt.payload = pktpos;
    pkt.payload_len = 0;

    return nanocoap_sock_request_cb(sock, &pkt, _get_put_cb, &ctx);
}

ssize_t nanocoap_sock_get(nanocoap_sock_t *sock, const char *path,
                          void *response, size_t len_max)
{
    return _sock_get(sock, path, COAP_TYPE_CON, response, len_max);
}

#ifdef MODULE_NANOCOAP_SOCK_OBSERVE
static void _async_udp_handler(sock_udp_t *sock, sock_async_flags_t type, void *arg)
{
    if (!(type & SOCK_ASYNC_MSG_RECV)) {
        return;
    }

    coap_pkt_t pkt;
    void *payload, *ctx = NULL;
    coap_observe_client_t *obs = arg;
    ssize_t res = sock_udp_recv_buf(sock, &payload, &ctx, 0, NULL);
    if (res <= 0) {
        return;
    }

    /* parse response */
    if (coap_parse(&pkt, payload, res) < 0) {
        DEBUG("nanocoap: error parsing packet\n");
        goto out;
    }

    DEBUG("nanocoap: response code=%i\n", coap_get_code_decimal(&pkt));
    switch (coap_get_type(&pkt)) {
    case COAP_TYPE_CON:
        _send_ack(&obs->sock, &pkt);
        /* fall-through */
    case COAP_TYPE_NON:
        obs->cb(obs->arg, &pkt);
        break;
    default:
        DEBUG("nanocoap: ignore observe pkt of invalid type %u\n", coap_get_type(&pkt));
        break;
    }
out:
    /* release data */
    sock_udp_recv_buf(sock, &payload, &ctx, 0, NULL);
}

static int _observe_reg_wrapper(void *arg, coap_pkt_t *pkt)
{
    coap_observe_client_t *obs = arg;
    bool registered = coap_find_option(pkt, COAP_OPT_OBSERVE);
    int res = obs->cb(obs->arg, pkt);

    return registered ? res : -EPROTONOSUPPORT;
}

static ssize_t _get_observe(coap_observe_client_t *ctx, const char *path,
                            bool unregister)
{
    /* buffer for CoAP header */
    uint8_t buffer[CONFIG_NANOCOAP_BLOCK_HEADER_MAX];
    uint8_t *pktpos = buffer;

    coap_pkt_t pkt = {
        .buf = pktpos,
    };

    uint16_t lastonum = 0;

    pktpos += coap_build_udp_hdr(buffer, sizeof(buffer), COAP_TYPE_CON, NULL, 0, COAP_METHOD_GET,
                                 nanocoap_sock_next_msg_id(&ctx->sock));
    pktpos += coap_opt_put_observe(pktpos, lastonum, unregister);
    lastonum = COAP_OPT_OBSERVE;
    pktpos += coap_opt_put_uri_pathquery(pktpos, &lastonum, path);

    pkt.payload = pktpos;
    pkt.payload_len = 0;

    return nanocoap_sock_request_cb(&ctx->sock, &pkt, _observe_reg_wrapper, ctx);
}

ssize_t nanocoap_sock_observe_url(const char *url, coap_observe_client_t *ctx,
                                  coap_request_cb_t cb, void *arg)
{
    int res = nanocoap_sock_url_connect(url, &ctx->sock);
    if (res) {
        return res;
    }
    ctx->cb  = cb;
    ctx->arg = arg;

    res = _get_observe(ctx, sock_urlpath(url), false);
    if (res >= 0) {
        sock_udp_event_init(&ctx->sock.udp, CONFIG_NANOCOAP_SOCK_EVENT_PRIO,
                            _async_udp_handler, ctx);
    }
    else {
        nanocoap_sock_close(&ctx->sock);
        ctx->cb = NULL;
    }

    return res;
}

ssize_t nanocoap_sock_unobserve_url(const char *url, coap_observe_client_t *ctx)
{
    if (ctx->cb == NULL) {
        return -ENOTCONN;
    }

    int res = _get_observe(ctx, sock_urlpath(url), true);

    /* we expect no observe option in the response */
    if (res == -EPROTONOSUPPORT) {
        res = 0;
    }

    nanocoap_sock_close(&ctx->sock);
    ctx->cb = NULL;
    return res;
}
#endif /* MODULE_NANOCOAP_SOCK_OBSERVE */

ssize_t _sock_put_post(nanocoap_sock_t *sock, const char *path, unsigned code,
                       uint8_t type, const void *request, size_t len,
                       void *response, size_t max_len)
{
    uint8_t *pktpos = sock->hdr_buf;

    iolist_t payload = {
        .iol_base = (void *)request,
        .iol_len  = len,
    };

    coap_pkt_t pkt = {
        .buf = pktpos,
        .snips = &payload,
    };

    struct iovec ctx = {
        .iov_base = response,
        .iov_len  = max_len,
    };

    uint16_t lastonum = 0;
    ssize_t hdr_len = coap_build_udp_hdr(sock->hdr_buf, sizeof(sock->hdr_buf), type, NULL, 0, code, nanocoap_sock_next_msg_id(sock));
    assume(hdr_len > 0);
    pktpos += hdr_len;
    pktpos += coap_opt_put_uri_pathquery(pktpos, &lastonum, path);

    if (response == NULL && type == COAP_TYPE_NON) {
        /* all responses (2.xx, 4.xx and 5.xx) are ignored */
        pktpos += coap_opt_put_uint(pktpos, lastonum,
                                    COAP_OPT_NO_RESPONSE, 26);
    }

    if (len) {
        /* set payload marker */
        *pktpos++ = 0xFF;
    }
    assert(pktpos < (uint8_t *)sock->hdr_buf + sizeof(sock->hdr_buf));

    pkt.payload = pktpos;
    pkt.payload_len = 0;

    return nanocoap_sock_request_cb(sock, &pkt, response ? _get_put_cb : NULL, &ctx);
}

ssize_t nanocoap_sock_put(nanocoap_sock_t *sock, const char *path,
                          const void *request, size_t len,
                          void *response, size_t len_max)
{
    return _sock_put_post(sock, path, COAP_METHOD_PUT, COAP_TYPE_CON, request, len,
                          response, len_max);
}

ssize_t nanocoap_sock_post(nanocoap_sock_t *sock, const char *path,
                           const void *request, size_t len,
                           void *response, size_t len_max)
{
    return _sock_put_post(sock, path, COAP_METHOD_POST, COAP_TYPE_CON, request, len,
                          response, len_max);
}

ssize_t nanocoap_sock_fetch(nanocoap_sock_t *sock, const char *path,
                            const void *request, size_t len,
                            void *response, size_t len_max)
{
    return _sock_put_post(sock, path, COAP_METHOD_FETCH, COAP_TYPE_CON, request, len,
                          response, len_max);
}

ssize_t nanocoap_sock_put_non(nanocoap_sock_t *sock, const char *path,
                              const void *request, size_t len,
                              void *response, size_t len_max)
{
    return _sock_put_post(sock, path, COAP_METHOD_PUT, COAP_TYPE_NON, request, len,
                          response, len_max);
}

ssize_t nanocoap_sock_post_non(nanocoap_sock_t *sock, const char *path,
                               const void *request, size_t len,
                               void *response, size_t len_max)
{
    return _sock_put_post(sock, path, COAP_METHOD_POST, COAP_TYPE_NON, request, len,
                          response, len_max);
}

ssize_t nanocoap_sock_fetch_non(nanocoap_sock_t *sock, const char *path,
                               const void *request, size_t len,
                               void *response, size_t len_max)
{
    return _sock_put_post(sock, path, COAP_METHOD_FETCH, COAP_TYPE_NON, request, len,
                          response, len_max);
}

ssize_t nanocoap_sock_get_non(nanocoap_sock_t *sock, const char *path,
                              void *response, size_t len_max)
{
    return _sock_get(sock, path, COAP_TYPE_NON, response, len_max);
}

static ssize_t _sock_put_post_url(const char *url, unsigned code,
                                  const void *request, size_t len,
                                  void *response, size_t len_max)
{
    nanocoap_sock_t sock;
    int res = nanocoap_sock_url_connect(url, &sock);
    if (res) {
        return res;
    }

    res = _sock_put_post(&sock, sock_urlpath(url), code, COAP_TYPE_CON,
                         request, len, response, len_max);
    nanocoap_sock_close(&sock);

    return res;
}

ssize_t nanocoap_sock_put_url(const char *url,
                              const void *request, size_t len,
                              void *response, size_t len_max)
{
    return _sock_put_post_url(url, COAP_METHOD_PUT, request, len, response, len_max);
}

ssize_t nanocoap_sock_post_url(const char *url,
                               const void *request, size_t len,
                               void *response, size_t len_max)
{
    return _sock_put_post_url(url, COAP_METHOD_POST, request, len, response, len_max);
}

ssize_t nanocoap_sock_fetch_url(const char *url,
                                const void *request, size_t len,
                                void *response, size_t len_max)
{
    return _sock_put_post_url(url, COAP_METHOD_FETCH, request, len, response, len_max);
}

ssize_t nanocoap_sock_delete(nanocoap_sock_t *sock, const char *path)
{
    uint8_t *pktpos = sock->hdr_buf;

    coap_pkt_t pkt = {
        .buf = pktpos,
    };

    ssize_t hdr_len = coap_build_udp_hdr(sock->hdr_buf, sizeof(sock->hdr_buf), COAP_TYPE_CON, NULL, 0,
                                         COAP_METHOD_DELETE, nanocoap_sock_next_msg_id(sock));
    assume(hdr_len > 0);
    pktpos += hdr_len;
    pktpos += coap_opt_put_uri_pathquery(pktpos, NULL, path);
    assert(pktpos < (uint8_t *)sock->hdr_buf + sizeof(sock->hdr_buf));

    pkt.payload = pktpos;

    return nanocoap_sock_request_cb(sock, &pkt, NULL, NULL);
}

ssize_t nanocoap_sock_delete_url(const char *url)
{
    nanocoap_sock_t sock;
    int res = nanocoap_sock_url_connect(url, &sock);
    if (res) {
        return res;
    }

    res = nanocoap_sock_delete(&sock, sock_urlpath(url));
    nanocoap_sock_close(&sock);

    return res;
}

ssize_t nanocoap_request(coap_pkt_t *pkt, const sock_udp_ep_t *local,
                         const sock_udp_ep_t *remote, size_t len)
{
    int res;
    nanocoap_sock_t sock;

    res = nanocoap_sock_connect(&sock, local, remote);
    if (res) {
        return res;
    }

    res = nanocoap_sock_request(&sock, pkt, len);
    nanocoap_sock_close(&sock);

    return res;
}

static int _block_cb(void *arg, coap_pkt_t *pkt)
{
    _block_ctx_t *ctx = arg;
    coap_block1_t block2;

    int res = _get_error(pkt);
    if (res) {
        return res;
    }

    /* response was not block-wise */
    if (!coap_get_block2(pkt, &block2)) {
        block2.offset = 0;
        block2.more = false;
    }

    DEBUG("nanocoap: got block %"PRIu32" (offset %u)\n",
          block2.blknum, (unsigned)block2.offset);

    if (block2.blknum != ctx->blknum) {
        return -EAGAIN;
    }

    ctx->more = block2.more;
    return ctx->callback(ctx->arg, block2.offset, pkt->payload, pkt->payload_len, block2.more);
}

static int _fetch_block(nanocoap_sock_t *sock, uint8_t *buf, size_t len,
                        const char *path, coap_blksize_t blksize,
                        _block_ctx_t *ctx)
{
    coap_pkt_t pkt = {
        .buf = buf,
    };
    uint16_t lastonum = 0;

    void *token = NULL;
    size_t token_len = 0;

#if CONFIG_NANOCOAP_SOCK_BLOCK_TOKEN
    /* HACK: go-coap always expects a token */
    /* see https://github.com/plgd-dev/go-coap/issues/512 */
    token = ctx->token;
    token_len = sizeof(ctx->token);
#endif

    ssize_t hdr_len = coap_build_udp_hdr(pkt.buf, len, COAP_TYPE_CON, token, token_len,
                                         COAP_METHOD_GET, nanocoap_sock_next_msg_id(sock));
    assume(hdr_len > 0);
    buf += hdr_len;
    buf += coap_opt_put_uri_pathquery(buf, &lastonum, path);
    buf += coap_opt_put_uint(buf, lastonum, COAP_OPT_BLOCK2, (ctx->blknum << 4) | blksize);

    assume((uintptr_t)buf - (uintptr_t)pkt.buf < len);

    pkt.payload = buf;
    pkt.payload_len = 0;

    return nanocoap_sock_request_cb(sock, &pkt, _block_cb, ctx);
}

int nanocoap_sock_block_request(coap_block_request_t *req,
                                const void *data, size_t len, bool more,
                                coap_request_cb_t callback, void *arg)
{
    /* clip the payload at the block size */
    if (len > coap_szx2size(req->blksize)) {
        len = coap_szx2size(req->blksize);
        more = true;
    }

    int res;
    iolist_t snip = {
        .iol_base = (void *)data,
        .iol_len  = len,
    };

    coap_pkt_t pkt = {
        .buf = req->sock->hdr_buf,
        .snips = &snip,
    };

    uint8_t *pktpos = pkt.buf;
    uint16_t lastonum = 0;

    ssize_t hdr_size =  coap_build_udp_hdr(req->sock->hdr_buf, sizeof(req->sock->hdr_buf), COAP_TYPE_CON, NULL, 0, req->method,
                                           nanocoap_sock_next_msg_id(req->sock));
    assume(hdr_size > 0);
    pktpos += hdr_size;
    pktpos += coap_opt_put_uri_pathquery(pktpos, &lastonum, req->path);
    pktpos += coap_opt_put_uint(pktpos, lastonum, COAP_OPT_BLOCK1,
                                (req->blknum << 4) | req->blksize | (more ? 0x8 : 0));
    if (len) {
        /* set payload marker */
        *pktpos++ = 0xFF;
    }
    assert(pktpos < (uint8_t *)req->sock->hdr_buf + sizeof(req->sock->hdr_buf));

    pkt.payload = pktpos;
    pkt.payload_len = 0;

    res = nanocoap_sock_request_cb(req->sock, &pkt, callback, arg);
    if (res < 0) {
        return res;
    }

    ++req->blknum;
    return len;
}

int nanocoap_sock_get_blockwise(nanocoap_sock_t *sock, const char *path,
                                coap_blksize_t blksize,
                                coap_blockwise_cb_t callback, void *arg)
{
    _block_ctx_t ctx = {
        .callback = callback,
        .arg = arg,
        .more = true,
    };

#if CONFIG_NANOCOAP_SOCK_BLOCK_TOKEN
    random_bytes(ctx.token, sizeof(ctx.token));
#endif

    uint8_t retries = CONFIG_COAP_MAX_RETRANSMIT;
    while (ctx.more) {
        DEBUG("nanocoap: fetching block %"PRIu32"\n", ctx.blknum);

        int res = _fetch_block(sock, sock->hdr_buf, sizeof(sock->hdr_buf), path, blksize, &ctx);
        if (res == -EAGAIN) {
            if (--retries) {
                continue;
            }
            res = -EBADMSG;
        }
        if (res < 0) {
            DEBUG("nanocoap: error fetching block %"PRIu32": %d\n", ctx.blknum, res);
            return res;
        }

        ctx.blknum += 1;
        retries = CONFIG_COAP_MAX_RETRANSMIT;
    }

    return 0;
}

typedef struct {
    uint8_t *ptr;
    size_t len;
    size_t offset;
    size_t res;
} _buf_slice_t;

static int _2buf_slice(void *arg, size_t offset, uint8_t *buf, size_t len, int more)
{
    _buf_slice_t *ctx = arg;

    if (offset + len < ctx->offset) {
        return 0;
    }

    if (offset > ctx->offset + ctx->len) {
        return 0;
    }

    if (!ctx->len) {
        return 0;
    }

    offset = ctx->offset - offset;
    len = MIN(len - offset, ctx->len);

    memcpy(ctx->ptr, buf + offset, len);

    ctx->len -= len;
    ctx->ptr += len;
    ctx->offset += len;
    ctx->res += len;

    DEBUG("nanocoap: got %"PRIuSIZE" bytes, %"PRIuSIZE" bytes left (offset: %"PRIuSIZE")\n",
          len, ctx->len, offset);

    if (!more) {
        ctx->len = 0;
    }

    return 0;
}

static unsigned _num_blks(size_t offset, size_t len, coap_blksize_t szx)
{
    uint16_t mask = coap_szx2size(szx) - 1;
    uint8_t shift = szx + 4;
    size_t end = offset + len;

    unsigned num_blks = ((end >> shift) + !!(end & mask))
                      - ((offset >> shift) + !!(offset & mask));
    return num_blks;
}

int nanocoap_sock_get_slice(nanocoap_sock_t *sock, const char *path,
                            coap_blksize_t blksize, size_t offset,
                            void *dst, size_t len)
{
    /* try to find optimal blocksize */
    unsigned num_blocks = _num_blks(offset, len, blksize);
    for (uint8_t szx = 0; szx < blksize; ++szx) {
        if (_num_blks(offset, len, szx) <= num_blocks) {
            blksize = szx;
            break;
        }
    }

    _buf_slice_t dst_ctx = {
        .ptr = dst,
        .len = len,
        .offset = offset,
    };

    _block_ctx_t ctx = {
        .callback = _2buf_slice,
        .arg = &dst_ctx,
        .blknum = offset >> (blksize + 4),
        .more = true,
    };

#if CONFIG_NANOCOAP_SOCK_BLOCK_TOKEN
    random_bytes(ctx.token, sizeof(ctx.token));
#endif

    uint8_t retries = CONFIG_COAP_MAX_RETRANSMIT;
    while (dst_ctx.len) {
        DEBUG("nanocoap: fetching block %"PRIu32"\n", ctx.blknum);

        int res = _fetch_block(sock, sock->hdr_buf, sizeof(sock->hdr_buf), path, blksize, &ctx);
        if (res == -EAGAIN) {
            if (--retries) {
                continue;
            }
            res = -EBADMSG;
        }
        if (res < 0) {
            DEBUG("nanocoap: error fetching block %"PRIu32": %d\n", ctx.blknum, res);
            return res;
        }

        ctx.blknum += 1;
        retries = CONFIG_COAP_MAX_RETRANSMIT;
    }

    return dst_ctx.res;
}

int nanocoap_sock_url_connect(const char *url, nanocoap_sock_t *sock)
{
    char hostport[CONFIG_SOCK_HOSTPORT_MAXLEN];
    sock_udp_ep_t remote;

    bool is_coaps = false;

    if (IS_USED(MODULE_NANOCOAP_DTLS) && !strncmp(url, "coaps://", 8)) {
        DEBUG("nanocoap: CoAPS URL detected\n");
        is_coaps = true;
    }

    if (!is_coaps && strncmp(url, "coap://", 7)) {
        DEBUG("nanocoap: URL doesn't start with \"coap://\"\n");
        return -EINVAL;
    }

    if (sock_urlsplit(url, hostport, NULL) < 0) {
        DEBUG("nanocoap: invalid URL\n");
        return -EINVAL;
    }

    if (sock_udp_name2ep(&remote, hostport) < 0) {
        DEBUG("nanocoap: invalid URL\n");
        return -EINVAL;
    }

    if (!remote.port) {
        remote.port = is_coaps ? COAPS_PORT : COAP_PORT;
    }

    if (is_coaps) {
#if SOCK_HAS_IPV6
        /* tinydtls wants the interface to match */
        if (!remote.netif && sock_udp_ep_is_v6(&remote) &&
            ipv6_addr_is_link_local((ipv6_addr_t *)remote.addr.ipv6)) {
            netif_t *iface = netif_iter(NULL);
            if (iface == NULL) {
                return -ENODEV;
            }
            remote.netif = netif_get_id(iface);
        }

        sock_udp_ep_t local = SOCK_IPV6_EP_ANY;
        if (!sock_udp_ep_is_v6(&remote)) {
            local.family = AF_INET;
        }
#else
        sock_udp_ep_t local = SOCK_IPV4_EP_ANY;
#endif
        return nanocoap_sock_dtls_connect(sock, &local, &remote, CONFIG_NANOCOAP_SOCK_DTLS_TAG);
    } else {
        return nanocoap_sock_connect(sock, NULL, &remote);
    }
}

int nanocoap_get_blockwise_url(const char *url,
                               coap_blksize_t blksize,
                               coap_blockwise_cb_t callback, void *arg)
{
    nanocoap_sock_t sock;
    int res = nanocoap_sock_url_connect(url, &sock);
    if (res) {
        return res;
    }

    res = nanocoap_sock_get_blockwise(&sock, sock_urlpath(url), blksize, callback, arg);
    nanocoap_sock_close(&sock);

    return res;
}

typedef struct {
    uint8_t *ptr;
    size_t len;
} _buf_t;

static int _2buf(void *arg, size_t offset, uint8_t *buf, size_t len, int more)
{
    _buf_t *dst = arg;

    if (offset + len > dst->len) {
        return -ENOBUFS;
    }

    memcpy(dst->ptr + offset, buf, len);

    if (!more) {
        dst->len = offset + len;
    }

    return 0;
}

ssize_t nanocoap_get_blockwise_url_to_buf(const char *url,
                                          coap_blksize_t blksize,
                                          void *buf, size_t len)
{
    _buf_t _buf = { .ptr = buf, .len = len };
    int res = nanocoap_get_blockwise_url(url, blksize, _2buf, &_buf);

    return (res < 0) ? (ssize_t)res : (ssize_t)_buf.len;
}

ssize_t nanocoap_get_blockwise_to_buf(nanocoap_sock_t *sock, const char *path,
                                      coap_blksize_t blksize,
                                      void *buf, size_t len)
{
    _buf_t _buf = { .ptr = buf, .len = len };

    int res = nanocoap_sock_get_blockwise(sock, path, blksize, _2buf, &_buf);

    return (res < 0) ? (ssize_t)res : (ssize_t)_buf.len;
}

int nanocoap_server(sock_udp_ep_t *local, void *rsp_buf, size_t rsp_buf_len)
{
    sock_udp_t sock;
    sock_udp_ep_t remote;
    coap_request_ctx_t ctx = {
        .remote = &remote,
    };

    if (!local->port) {
        local->port = COAP_PORT;
    }

    ssize_t res = sock_udp_create(&sock, local, NULL, 0);
    if (res != 0) {
        return res;
    }

    void *buf;
    void *buf_ctx = NULL;

    while (1) {

        if (buf_ctx) {
            /* free the buffer */
            res = sock_udp_recv_buf_aux(&sock, &buf, &buf_ctx, 0, NULL, NULL);
            assert(res == 0);
        }

        sock_udp_aux_rx_t *aux_in_ptr = NULL;
#ifdef MODULE_SOCK_AUX_LOCAL
        sock_udp_aux_rx_t aux_in = {
            .flags = SOCK_AUX_GET_LOCAL,
        };
        aux_in_ptr = &aux_in;
#endif

        res = sock_udp_recv_buf_aux(&sock, &buf, &buf_ctx, SOCK_NO_TIMEOUT,
                                    &remote, aux_in_ptr);
        if (res <= 0) {
            DEBUG("nanocoap: error receiving UDP packet %" PRIdSIZE "\n", res);
            continue;
        }
        coap_pkt_t pkt;
        if (coap_parse_udp(&pkt, buf, res) < 0) {
            DEBUG("nanocoap: error parsing packet\n");
            continue;
        }

        sock_udp_aux_tx_t *aux_out_ptr = NULL;
#ifdef MODULE_SOCK_AUX_LOCAL
        /* make sure we reply with the same address that the request was
         * destined for -- except in the multicast case */
        sock_udp_aux_tx_t aux_out = {
            .flags = SOCK_AUX_SET_LOCAL,
            .local = aux_in.local,
        };
        if (!sock_udp_ep_is_multicast(&aux_in.local)) {
            aux_out_ptr = &aux_out;
        }
        ctx.local = &aux_in.local;
#endif
        if ((res = coap_handle_req(&pkt, rsp_buf, rsp_buf_len, &ctx)) <= 0) {
            DEBUG("nanocoap: error handling request %" PRIdSIZE "\n", res);
            continue;
        }

        sock_udp_send_aux(&sock, rsp_buf, res, &remote, aux_out_ptr);
    }

    return 0;
}

static kernel_pid_t _coap_server_pid;
static void *_nanocoap_server_thread(void *local)
{
    static uint8_t buf[CONFIG_NANOCOAP_SERVER_BUF_SIZE];

    nanocoap_server(local, buf, sizeof(buf));

    return NULL;
}

kernel_pid_t nanocoap_server_start(const sock_udp_ep_t *local)
{
    static char stack[CONFIG_NANOCOAP_SERVER_STACK_SIZE];

    if (_coap_server_pid) {
        return _coap_server_pid;
    }
    _coap_server_pid = thread_create(stack, sizeof(stack), THREAD_PRIORITY_MAIN - 1,
                                     0, _nanocoap_server_thread,
                                     (void *)local, "nanoCoAP server");
    return _coap_server_pid;
}

void auto_init_nanocoap_server(void)
{
    sock_udp_ep_t local = {
        .port = COAP_PORT,
        .family = AF_INET6,
    };

    nanocoap_server_start(&local);
}

#if MODULE_NANOCOAP_SERVER_SEPARATE
int nanocoap_server_prepare_separate(nanocoap_server_response_ctx_t *ctx,
                                     coap_pkt_t *pkt, const coap_request_ctx_t *req)
{
    size_t tkl = coap_get_token_len(pkt);
    if (tkl > sizeof(ctx->token)) {
        DEBUG_PUTS("nanocoap: token too long for separate response ctx");
        /* Legacy code may not check the return value. To still have somewhat
         * sane behavior, we ask for no response for any response class.
         * Getting no reply is certainly not ideal, but better than one without
         * a matching token. */
        memset(ctx, 0, sizeof(*ctx));
        ctx->no_response = 0xff;
        return -EOVERFLOW;
    }
    ctx->tkl = tkl;
    memcpy(ctx->token, coap_get_token(pkt), tkl);
    memcpy(&ctx->remote, req->remote, sizeof(ctx->remote));
    assert(req->local);
    memcpy(&ctx->local, req->local, sizeof(ctx->local));
    uint32_t no_response = 0;
    coap_opt_get_uint(pkt, COAP_OPT_NO_RESPONSE, &no_response);
    ctx->no_response = no_response;

    return 0;
}

bool nanocoap_server_is_remote_in_response_ctx(const nanocoap_server_response_ctx_t *ctx,
                                               const coap_request_ctx_t *req)
{
    return sock_udp_ep_equal(&ctx->remote, req->remote);
}

ssize_t nanocoap_server_build_separate(const nanocoap_server_response_ctx_t *ctx,
                                       void *buf, size_t buf_len,
                                       unsigned code, unsigned type,
                                       uint16_t msg_id)
{
    assert(type != COAP_TYPE_ACK);
    assert(type != COAP_TYPE_CON); /* TODO: add support */
    if ((sizeof(coap_hdr_t) + COAP_TOKEN_LENGTH_MAX + 1) > buf_len) {
        return -EOVERFLOW;
    }

    const uint8_t no_response_index = (code >> 5) - 1;
    /* If the handler code misbehaved here, we'd face UB otherwise */
    assert(no_response_index < 7);

    const uint8_t mask = 1 << no_response_index;
    if (ctx->no_response & mask) {
        return -ECANCELED;
    }

    return coap_build_hdr(buf, type, ctx->token, ctx->tkl, code, msg_id);
}

int nanocoap_server_sendv_separate(const nanocoap_server_response_ctx_t *ctx,
                                   const iolist_t *reply)
{
    sock_udp_aux_tx_t *aux_out_ptr = NULL;
    /* make sure we reply with the same address that the request was
     * destined for -- except in the multicast case */
    sock_udp_aux_tx_t aux_out = {
        .flags = SOCK_AUX_SET_LOCAL,
        .local = ctx->local,
    };
    if (!sock_udp_ep_is_multicast(&ctx->local)) {
        aux_out_ptr = &aux_out;
    }
    ssize_t retval = sock_udp_sendv_aux(NULL, reply, &ctx->remote, aux_out_ptr);

    if (retval < 0) {
        return retval;
    }

    return 0;
}

int nanocoap_server_send_separate(const nanocoap_server_response_ctx_t *ctx,
                                  unsigned code, unsigned type,
                                  const void *payload, size_t len)
{
    uint8_t rbuf[sizeof(coap_hdr_t) + COAP_TOKEN_LENGTH_MAX + 1];

    ssize_t hdr_len = nanocoap_server_build_separate(ctx, rbuf, sizeof(rbuf),
                                                     code, type, random_uint32());
    if (hdr_len < 0) {
        return hdr_len;
    }

    /* add payload marker if needed */
    if (len) {
        rbuf[hdr_len++] = 0xFF;
    }

    iolist_t data = {
        .iol_base = (void *)payload,
        .iol_len  = len,
    };

    iolist_t head = {
        .iol_next = &data,
        .iol_base = rbuf,
        .iol_len = hdr_len,
    };

    return nanocoap_server_sendv_separate(ctx, &head);
}
#endif

#if MODULE_NANOCOAP_SERVER_OBSERVE
int nanocoap_register_observer(const coap_request_ctx_t *req_ctx, coap_pkt_t *req_pkt)
{
    mutex_lock(&_observer_pool_lock);

    _observer_t *free = NULL;
    const coap_resource_t *resource = req_ctx->resource;

    for (size_t i = 0; i < CONFIG_NANOCOAP_MAX_OBSERVERS; i++) {
        if (_observer_pool[i].resource == NULL) {
            free = &_observer_pool[i];
        }
        if ((_observer_pool[i].resource == resource)
                && sock_udp_ep_equal(&_observer_pool[i].response.remote,
                                     coap_request_ctx_get_remote_udp(req_ctx)))
        {
            /* Deviation from the standard: Subscribing twice makes no
             * sense with our CoAP implementation, so either this is a
             * reaffirmation of an existing subscription (same token) or the
             * client lost state (different token). We just update the
             * subscription in either case */
            DEBUG("nanocoap: observe slot %" PRIuSIZE " reused\n", i);
            uint8_t tkl = coap_get_token_len(req_pkt);
            _observer_pool[i].response.tkl = tkl;
            memcpy(_observer_pool[i].response.token, coap_get_token(req_pkt), tkl);
            mutex_unlock(&_observer_pool_lock);
            return 0;
        }
    }

    if (!free) {
        DEBUG_PUTS("nanocoap: observe registration failed, no free slot");
        mutex_unlock(&_observer_pool_lock);
        return -ENOMEM;
    }

    int retval = nanocoap_server_prepare_separate(&free->response, req_pkt, req_ctx);
    if (retval) {
        DEBUG("nanocoap: observe registration failed: %d\n", retval);
        mutex_unlock(&_observer_pool_lock);
        return retval;
    }
    free->resource = req_ctx->resource;
    free->msg_id = random_uint32();
    mutex_unlock(&_observer_pool_lock);
    DEBUG("nanocoap: new observe registration at slot %" PRIuSIZE "\n",
          index_of(_observer_pool, free));
    return 0;
}

void nanocoap_unregister_observer(const coap_request_ctx_t *req_ctx,
                                  const coap_pkt_t *req_pkt)
{
    mutex_lock(&_observer_pool_lock);
    for (size_t i = 0; i < CONFIG_NANOCOAP_MAX_OBSERVERS; i++) {
        if ((_observer_pool[i].resource == req_ctx->resource)
                && (_observer_pool[i].response.tkl == coap_get_token_len(req_pkt))
                && !memcmp(_observer_pool[i].response.token, coap_get_token(req_pkt),
                           _observer_pool[i].response.tkl)
                && sock_udp_ep_equal(&_observer_pool[i].response.remote, coap_request_ctx_get_remote_udp(req_ctx))) {
            DEBUG("nanocoap: observer at index %" PRIuSIZE " unregistered\n", i);
            _observer_pool[i].resource = NULL;
        }
    }
    mutex_unlock(&_observer_pool_lock);
}

void nanocoap_unregister_observer_due_to_reset(const sock_udp_ep_t *ep,
                                               uint16_t msg_id)
{
    mutex_lock(&_observer_pool_lock);
    for (size_t i = 0; i < CONFIG_NANOCOAP_MAX_OBSERVERS; i++) {
        if ((_observer_pool[i].resource != NULL)
                && (_observer_pool[i].msg_id == msg_id)
                && sock_udp_ep_equal(&_observer_pool[i].response.remote, ep)) {
            DEBUG("nanocoap: observer at index %" PRIuSIZE " unregistered due to RST\n", i);
            _observer_pool[i].resource = NULL;
            return;
        }
    }
    mutex_unlock(&_observer_pool_lock);
}

void nanocoap_notify_observers(const coap_resource_t *res, const iolist_t *iol)
{
    mutex_lock(&_observer_pool_lock);
    for (size_t i = 0; i < CONFIG_NANOCOAP_MAX_OBSERVERS; i++) {
        if (_observer_pool[i].resource == res) {
            uint8_t rbuf[sizeof(coap_hdr_t) + COAP_TOKEN_LENGTH_MAX + 1];

            ssize_t hdr_len = nanocoap_server_build_separate(&_observer_pool[i].response, rbuf, sizeof(rbuf),
                                                             COAP_CODE_CONTENT, COAP_TYPE_NON,
                                                             ++_observer_pool[i].msg_id);
            if (hdr_len < 0) {
                /* no need to keep the observer in the pool, if we cannot
                 * send anyway */
                _observer_pool[i].resource = NULL;
                continue;
            }

            const iolist_t msg = {
                .iol_base = rbuf,
                .iol_len = hdr_len,
                .iol_next = (iolist_t *)iol
            };

            if (nanocoap_server_sendv_separate(&_observer_pool[i].response, &msg)) {
                /* no need to keep the observer in the pool, if we cannot
                 * send anyway */
                _observer_pool[i].resource = NULL;
            }
        }
    }
    mutex_unlock(&_observer_pool_lock);
}
#endif
